Using CW-Analyzer for CPA Attack

This tutorial will take you through a complete attack on a software AES implementation. The specific implementation being attacked is a well-known AES implementation written in C, which is likely to be similar to other implementations used by proprietary systems.

Capturing Power Traces

Setup

We'll use some helper scripts to make setup and programming easier. If you're using an XMEGA or STM (CWLITEARM) target, binaries with the correct should be setup for you:

In [1]:
import chipwhisperer as cw
scope = cw.scope()
target = cw.target(scope)
In [2]:
%run "Helper_Scripts/Setup_Target_Generic.ipynb"
In [3]:
# uncomment based on your target
#%run "Helper_Scripts/Program_XMEGA.ipynb"
%run "Helper_Scripts/Program_STM.ipynb"
#%run "Helper_Scripts/No_Programmer.ipynb"
fw_path = "../../hardware/victims/firmware/simpleserial-aes/simpleserial-aes-cwlitearm.hex"
In [4]:
# program the target
program_target(scope, fw_path)
Detected known STMF32: STM32F302xB(C)/303xB(C)
Extended erase (0x44), this can take ten seconds or more
Attempting to programming 5879 bytes at 0x8000000
STM32F Programming flash...
STM32F Reading flash...
Verified flash OK, 5879 bytes

In addition, before we capture our traces, we'll need to create a ChipWhipserer project, since that's what Analyzer expects for an input:

In [5]:
project = cw.createProject("projects/Tutorial_B5.cwp", overwrite = True)

And we can get the class used to hold our traces by:

In [6]:
tc = project.getTraceFormat()

Capturing Traces

Below you can see the capture loop. The main body of the loop loads some new plaintext, arms the scope, sends the key and plaintext, then finally records and our new trace into our trace class.

In [7]:
#Capture Traces
from tqdm import tqdm
import numpy as np
import time

ktp = cw.ktp.Basic(target=target)

N = 50  # Number of traces
target.init()
for i in tqdm(range(N), desc='Capturing traces'):
    # run aux stuff that should come before trace here

    key, text = ktp.newPair()  # manual creation of a key, text pair can be substituted here

    #target.reinit()

    target.setModeEncrypt()  # only does something for targets that support it
    target.loadEncryptionKey(key)
    target.loadInput(text)

    # run aux stuff that should run before the scope arms here

    scope.arm()

    # run aux stuff that should run after the scope arms here

    target.go()
    timeout = 50
    # wait for target to finish
    while target.isDone() is False and timeout:
        timeout -= 1
        time.sleep(0.01)

    ret = scope.capture()
    if ret:
        print('Timeout happened during acquisition')

    # run aux stuff that should happen after trace here
    _ = target.readOutput()  # clears the response from the serial port
    #traces.append(scope.getLastTrace())
    tc.addTrace(scope.getLastTrace(), text, "", key)
Capturing traces: 100%|██████████| 50/50 [00:09<00:00,  4.95it/s]

Now that we have our traces, we need to tell the project that the traces are loaded and add them to the project's trace manager.

In [8]:
tc._isloaded = True
project.traceManager().appendSegment(tc)

If you'd like, you can also save the project for later analysis (this closes the project, so if you run this block you'll need to reopen it in the next section):

In [9]:
from datetime import datetime
import copy

starttime = datetime.now()
prefix = starttime.strftime('%Y.%m.%d-%H.%M.%S') + "_"
tc.config.setConfigFilename(project.datadirectory + "traces/config_" + prefix + ".cfg")
tc.config.setAttr("prefix", prefix)
tc.config.setAttr("date", starttime.strftime('%Y-%m-%d %H:%M:%S'))
tc.closeAll()
project.save()

We're now done with the ChipWhisperer hardware, so we should disconnect from the scope and target:

In [10]:
# cleanup the connection to the target and scope
scope.dis()
target.dis()

Analysis

If you saved in the last part (or if you're continuing from where you left off), you'll need to reload the project:

In [11]:
import chipwhisperer as cw
project = cw.openProject("projects/Tutorial_B5.cwp")
tm = project.traceManager()

Now that we have our traces, we can begin our attack! We'll start off by setting up our attack:

In [12]:
attack = cw.CPA()
N = 50 #number of traces

leak_model = cw.AES128(cw.AES128Leakage.SBox_output)
attack.setAnalysisAlgorithm(cw.CPAProgressive, leak_model)
attack.setTraceSource(tm)
attack.setTraceStart(0)
attack.setTracesPerAttack(tm.numTraces())
attack.setIterations(1)
attack.setReportingInterval(10)
attack.setTargetSubkeys([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
attack.setPointRange((0, -1))

And then actually run it:

In [13]:
attack_results = attack.processTracesNoGUI()

Once you see the above block complete, all the heavylifting is done! All that's left is to actually look at the data. Everything important is contained in the attack_results class that attack.processTracesNoGUI() returned.

We can find the max correlation for every one of the subkey by calling stats.findMaximums(), which returns a list of the subkeys, the point location of the max if calculated, and the correlation (which is a value between 0 and 1 that effectively tells us how well our guess fit the data).

Note the "point location of the max" is normally not calculated/tracked, and thus returns as a 0. Using the pandas library lets us print them nicely in a DataFrame. We have to transpose the frame to get our expected orientation:

In [14]:
import pandas as pd
stat_data = attack_results.findMaximums()
df = pd.DataFrame(stat_data).transpose()
print(df.head())
                             0                             1   \
0   [43, 0, 0.9253351279292428]  [126, 0, 0.8549739892526729]   
1     [88, 0, 0.63295558458046]   [68, 0, 0.6111016237073975]   
2  [213, 0, 0.6265362738838126]  [145, 0, 0.6071920625185497]   
3   [83, 0, 0.6237946516470981]    [65, 0, 0.603562609195244]   
4  [136, 0, 0.5993795741644136]   [80, 0, 0.6017696326441984]   

                            2                             3   \
0  [21, 0, 0.8473445885420205]   [22, 0, 0.8284483360154901]   
1   [1, 0, 0.6440441682145489]  [206, 0, 0.6427300638441308]   
2  [79, 0, 0.6355844952991977]     [5, 0, 0.621912130920625]   
3  [28, 0, 0.6344406589853376]  [182, 0, 0.6134842020951867]   
4  [82, 0, 0.6281995220410134]  [121, 0, 0.6112283647675913]   

                             4                             5   \
0    [40, 0, 0.938110343370555]  [174, 0, 0.8964079229981912]   
1   [59, 0, 0.6311694110337293]   [52, 0, 0.6146260336129882]   
2  [224, 0, 0.6281946162330312]   [29, 0, 0.6104035713108432]   
3  [154, 0, 0.6071607751859202]  [180, 0, 0.5970307273226447]   
4  [253, 0, 0.6066533042603154]   [62, 0, 0.5931612367506129]   

                             6                             7   \
0  [210, 0, 0.8858630612169691]  [166, 0, 0.8554428070175557]   
1   [204, 0, 0.651371224507917]    [7, 0, 0.6182343185914433]   
2   [165, 0, 0.626838256333804]  [225, 0, 0.6153669437680402]   
3   [36, 0, 0.6261728110776096]   [96, 0, 0.6084775247080669]   
4  [248, 0, 0.6098108977960224]  [154, 0, 0.5941233387734859]   

                             8                             9   \
0  [171, 0, 0.9261142321243571]  [247, 0, 0.8372227657565107]   
1  [125, 0, 0.6234840759723532]  [157, 0, 0.6140999248592186]   
2  [180, 0, 0.6175612156025521]   [162, 0, 0.602377833877816]   
3   [59, 0, 0.6096226029658068]   [12, 0, 0.5938266038484948]   
4  [183, 0, 0.5949785081791088]    [59, 0, 0.588828483812095]   

                             10                            11  \
0   [21, 0, 0.8762635452069677]  [136, 0, 0.8239976962023228]   
1  [115, 0, 0.6352697340164923]  [183, 0, 0.6462876972572236]   
2   [46, 0, 0.6273375786884614]   [34, 0, 0.6203588389955595]   
3  [255, 0, 0.6117945171810502]   [91, 0, 0.6180867523476447]   
4  [225, 0, 0.6024216423929245]  [211, 0, 0.6146420088775122]   

                             12                            13  \
0    [9, 0, 0.9250106849772386]  [207, 0, 0.8485194624289017]   
1   [42, 0, 0.6515371840963213]  [253, 0, 0.6056498682416163]   
2   [15, 0, 0.6351924104730677]  [243, 0, 0.5994900332357539]   
3  [210, 0, 0.6222024398107615]   [17, 0, 0.5932139368360771]   
4   [11, 0, 0.6173186116826618]   [64, 0, 0.5890333654167351]   

                             14                            15  
0   [79, 0, 0.9029645086655335]    [60, 0, 0.839785268625628]  
1  [196, 0, 0.6752190725241136]   [50, 0, 0.6652690550426914]  
2  [194, 0, 0.6653495607102579]  [172, 0, 0.6323210837468057]  
3  [214, 0, 0.6448285024752678]  [237, 0, 0.6281878310094956]  
4  [166, 0, 0.6349386316194825]  [189, 0, 0.6179437371591995]  

Even better, we can use the .style method to customize this further. This also lets us chain formatting functions. For example, we can remove the extra 0 and clean up the data. Since we know the correct key, we can even do things like printing the key in a different colour!

You can do lots of formatting thanks to the pandas library! Check out https://pandas.pydata.org/pandas-docs/stable/style.html for more details.

In [15]:
key = project.traceManager().getKnownKey(0)
def format_stat(stat):
    return str("{:02X}<br>{:.3f}".format(stat[0], stat[2]))

def color_corr_key(row):
    global key
    ret = [""] * 16
    for i,bnum in enumerate(row):
        if bnum[0] == key[i]:
            ret[i] = "color: red"
        else:
            ret[i] = ""
    return ret

df.head().style.format(format_stat).apply(color_corr_key, axis=1)
Out[15]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 2B
0.925
7E
0.855
15
0.847
16
0.828
28
0.938
AE
0.896
D2
0.886
A6
0.855
AB
0.926
F7
0.837
15
0.876
88
0.824
09
0.925
CF
0.849
4F
0.903
3C
0.840
1 58
0.633
44
0.611
01
0.644
CE
0.643
3B
0.631
34
0.615
CC
0.651
07
0.618
7D
0.623
9D
0.614
73
0.635
B7
0.646
2A
0.652
FD
0.606
C4
0.675
32
0.665
2 D5
0.627
91
0.607
4F
0.636
05
0.622
E0
0.628
1D
0.610
A5
0.627
E1
0.615
B4
0.618
A2
0.602
2E
0.627
22
0.620
0F
0.635
F3
0.599
C2
0.665
AC
0.632
3 53
0.624
41
0.604
1C
0.634
B6
0.613
9A
0.607
B4
0.597
24
0.626
60
0.608
3B
0.610
0C
0.594
FF
0.612
5B
0.618
D2
0.622
11
0.593
D6
0.645
ED
0.628
4 88
0.599
50
0.602
52
0.628
79
0.611
FD
0.607
3E
0.593
F8
0.610
9A
0.594
B7
0.595
3B
0.589
E1
0.602
D3
0.615
0B
0.617
40
0.589
A6
0.635
BD
0.618

You should see red numbers printed at the top of a table. Congratulations, you've now completed a successful CPA attack against AES!

Next, we'll look at how we can use some of Analyzer's other features to improve the attack process, as well as better interpret the data we have.

Reporting Intervals

When we ran attack.processTracesNoGUI(), we processed all of the traces before getting any information back. While this works okay for shorter attacks like this, for longer ones it can helpful to get feedback during the attack. This can be done by creating a callback function and passing it to attack.processTracesNoGUI(). This function is called each time we pass our attack.setReportingInterval() (in this case, every 10 traces) and has access to everything a normal python function does.

Let's use this to update our table every 10 traces. Most of this is just putting our existing code into the callback function. We also need use the clear_output function to clear the table, as well as display() to actually get it to show up:

In [16]:
from IPython.display import clear_output
import numpy as np
        
def stats_callback():
    attack_results = attack.getStatistics()
    attack_results.setKnownkey(key)
    stat_data = attack_results.findMaximums()
    df = pd.DataFrame(stat_data).transpose()
    clear_output(wait=True)
    display(df.head().style.format(format_stat).apply(color_corr_key,axis=1))
    
attack.setReportingInterval(10)
attack_results = attack.processTracesNoGUI(stats_callback)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 2B
0.925
7E
0.855
15
0.847
16
0.828
28
0.938
AE
0.896
D2
0.886
A6
0.855
AB
0.926
F7
0.837
15
0.876
88
0.824
09
0.925
CF
0.849
4F
0.903
3C
0.840
1 58
0.633
44
0.611
01
0.644
CE
0.643
3B
0.631
34
0.615
CC
0.651
07
0.618
7D
0.623
9D
0.614
73
0.635
B7
0.646
2A
0.652
FD
0.606
C4
0.675
32
0.665
2 D5
0.627
91
0.607
4F
0.636
05
0.622
E0
0.628
1D
0.610
A5
0.627
E1
0.615
B4
0.618
A2
0.602
2E
0.627
22
0.620
0F
0.635
F3
0.599
C2
0.665
AC
0.632
3 53
0.624
41
0.604
1C
0.634
B6
0.613
9A
0.607
B4
0.597
24
0.626
60
0.608
3B
0.610
0C
0.594
FF
0.612
5B
0.618
D2
0.622
11
0.593
D6
0.645
ED
0.628
4 88
0.599
50
0.602
52
0.628
79
0.611
FD
0.607
3E
0.593
F8
0.610
9A
0.594
B7
0.595
3B
0.589
E1
0.602
D3
0.615
0B
0.617
40
0.589
A6
0.635
BD
0.618

A default jupyter callback is also available:

In [17]:
import chipwhisperer as cw
cb = cw.getJupyterCallback(attack)
attack_results = attack.processTracesNoGUI(cb)
Finished traces 40 to 50
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
PGE= 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 2B
0.925
7E
0.855
15
0.847
16
0.828
28
0.938
AE
0.896
D2
0.886
A6
0.855
AB
0.926
F7
0.837
15
0.876
88
0.824
09
0.925
CF
0.849
4F
0.903
3C
0.840
1 58
0.633
44
0.611
01
0.644
CE
0.643
3B
0.631
34
0.615
CC
0.651
07
0.618
7D
0.623
9D
0.614
73
0.635
B7
0.646
2A
0.652
FD
0.606
C4
0.675
32
0.665
2 D5
0.627
91
0.607
4F
0.636
05
0.622
E0
0.628
1D
0.610
A5
0.627
E1
0.615
B4
0.618
A2
0.602
2E
0.627
22
0.620
0F
0.635
F3
0.599
C2
0.665
AC
0.632
3 53
0.624
41
0.604
1C
0.634
B6
0.613
9A
0.607
B4
0.597
24
0.626
60
0.608
3B
0.610
0C
0.594
FF
0.612
5B
0.618
D2
0.622
11
0.593
D6
0.645
ED
0.628
4 88
0.599
50
0.602
52
0.628
79
0.611
FD
0.607
3E
0.593
F8
0.610
9A
0.594
B7
0.595
3B
0.589
E1
0.602
D3
0.615
0B
0.617
40
0.589
A6
0.635
BD
0.618

Here we used a reporting interval of 10 traces. Depending on the attack and what you want to learn from it, you may want to use higher or lower values: in general reporting less often is faster, but more frequent reporting can allow you to end a long attack early. More frequent reporting also increases the resolution of some plot data (which we will look at next).

Plot Data

Analyzer also includes a module to create plots to help you interpret the data. These act on one subkey at a time and return some data that we can plot using bokeh (or your graphing module of choice). Let's start by grabbing the class that does all the calculations:

In [18]:
plot_data = cw.analyzerPlots(attack_results)

Output Vs. Time

We'll start by looking at the Output Vs. Time module, which will allow us to plot correlation of our guesses in time. This is useful for finding exactly where the operations we're attacking are. Like in previous tutorials, we'll use bokeh to plot the data we get back.

The method we're interested in is getPlotData(bnum), which returns in a list: [xrange, correct_key, incorrect_key_data, incorrect_key_data] for the position bnum passed to it. The method returns two sets of incorrect key data because one is for the key guesses below the correct one, and the other is for guesses above the correct one.

We'll start by just looking at the 0th subkey. Once we get this data back we'll plot the correct key in red, and the rest in green.

In [19]:
from bokeh.plotting import figure, show
from bokeh.io import output_notebook

ret = plot_data.outputVsTime(0)

output_notebook()
p = figure()
p.line(ret[0], ret[2], line_color='green')
p.line(ret[0], ret[3], line_color='green')

p.line(ret[0], ret[1], line_color='red')
show(p)
Loading BokehJS ...

You should see some distinctive red spikes in your plot. The largest of these is where the sbox lookup is actually happening (the smaller ones are typically other AES operations that move the sbox data around).

Let's repeat this for all the subkeys. This is quite a bit more data to plot, so give it a few seconds:

In [20]:
rets = []
for i in range(0, 16):
    rets.append(plot_data.outputVsTime(i))

p = figure()
for ret in rets:
    p.line(ret[0], ret[2], line_color='green')
    p.line(ret[0], ret[3], line_color='green')
    
for ret in rets:
    p.line(ret[0], ret[1], line_color='red')

show(p)

This information can be useful in many ways. For example, you can probably see the first 16 spikes that make up the sbox lookup are a small portion of the total trace length. If we ever needed to rerun the attack, we could capture a much smaller number of samples and speed up analysis significantly!

PGE vs. Traces

The next data we'll look at is a plot of partial guessing entropy (PGE) vs. the number of traces. As mentioned before, PGE is just how many spots away from the top the actual subkey is in our table of guesses. For example, if there are 7 subkey guesses that have a higher correlation than the actual subkey, the subkey has a PGE of 7.

This plot is useful for seeing how many traces were needed to actually break the AES implementation. Keep in mind, however, that the resolution of the plot is determined by the reporting interval (also note that attack_results.findMaximums() must be called in the callback function). In our case, we have a reporting interval of 10, so we'll have a resolution of 10 traces.

This module's getPlotData() is similar to the previous plot in that it takes bnum as an argument and returns a list of [xrange, PGE]. Plotting this data is quite a bit faster than the previous example, we'll just plot all 16 of the bnum now.

In [21]:
p = figure()

for bnum in range(16):
    ret = plot_data.pgeVsTrace(bnum)
    p.line(ret[0], ret[1], line_color='red')
show(p)

You should see a number of lines that start off with high values, then rapidly drop off. You may notice that we broke the AES implementation without needing to use all of our traces.

Even though we may have broken the AES implementation in fewer traces, we may not want to reduce how many traces we capture. Remember that, while we know the key here, for a real attack we won't and therefore must use the correlation to determine when we've broken a key. Our next plot will help us to determine how feesible capturing fewer traces is.

Correlation vs. Traces

The last plot we'll take a look at is correlation vs the number of traces. Like with PGE vs. Traces, this plot's resolution is determined by the reporting interval (10 in our case). One again, this is a plot with a lot of data, so we'll start of by just plotting one subkey:

This module's getPlotData() returns a list of [xrange, [data_for_kguess]], so we'll need to plot each guess for each subkey. Like before, we'll do the plot for the correct subkey in red and the rest in green.

In [22]:
ret = plot_data.corrVsTrace(0)
p = figure()
for i in range(255):
    if i == key[0]:
        p.line(ret[0], ret[1][i], line_color='red')
    else:
        p.line(ret[0], ret[1][i], line_color='green')
        
show(p)

As you can see, all the subkey guesses start of with large correlations, but all of them except for the correct guess quickly drop off. If you didn't know the key, at what point would you be sure that the guess with the highest correlation was actually the correct subkey?

Let's continue and plot all of the subkeys (give this one some time):

In [23]:
p = figure()
for bnum in range(16):
    ret = plot_data.corrVsTrace(bnum)
    for i in range(255):
        if i == key[bnum]:
            p.line(ret[0], ret[1][i], line_color='red')
        else:
            p.line(ret[0], ret[1][i], line_color='green')
            
show(p)

Like in the first plot, you should see the red lines remain high while the green ones drop off. At what point would you be sure that you've broken all the subkeys? Is it higher than when all of the PGE lines reached zero?

Conclusion

You should now have completed a successful CPA attack and learned about some on Analyzer's features for improving your attack!

You can move onto more advanced tutorials, especially showing you how the actual attack works when performed manually (Tutorial B6). This tutorial also utilized tiny-AES128-C for Arm targets, which uses the same operations as the XMEGA target. A later tutorial will preform this attack on a more typical 32 bit AES implementation.

Tests

In [24]:
key = project.traceManager().getKnownKey(0)
recv_key = [kguess[0][0] for kguess in attack_results.findMaximums()]
assert (key == recv_key).all(), "Failed to recover encryption key\nGot: {}\nExpected: {}".format(recv_key, key)
In [25]:
assert (attack_results.pge == [0]*16), "PGE for some bytes not zero: {}".format(attack_results.pge)
In [26]:
max_corrs = [kguess[0][2] for kguess in attack_results.findMaximums()]
assert (np.all([corr > 0.75 for corr in max_corrs])), "Low correlation in attack (corr <= 0.75): {}".format(max_corrs)
In [ ]: